import React, { useState, useEffect, useRef, useCallback, createContext, useContext, FC } from 'react'
import ReactDOM from 'react-dom'
import styled from 'styled-components'

export type DnDContextValue = {
	placeholderIndex: number | null
	setPlaceholderIndex: React.Dispatch<React.SetStateAction<number | null>>
}

export interface DraggableProps {
	style?: React.CSSProperties
	className?: string
	index?: number
	/**
	 * dataTransfer会进行JSON.stringify处理。
	 * 请将传入的 domain数据或者其他stringify后会丢失的数据进行处理/还原，防止转换后丢失。
	 */
	dataTransfer: object | (() => object)
	ghostRender: React.ReactNode
	canDrag?: boolean
	onDragStart?: (ev: React.DragEvent) => void
	onDragEnd?: (ev: React.DragEvent) => void
	children: React.ReactNode
}

export interface DroppableProps<T> {
	isPlaceholder: T
	placeholderHeight: number | number[]
	onDrop(dataTransfer: object, placeholderIndex: T extends true ? number : null, ev: DragEvent): void
	children: (setDroppableRef: (value: HTMLElement | null) => void) => React.ReactNode
}

const Wrap = styled.div`
	transition: transform 0.2s cubic-bezier(0.2, 0, 0, 1) 0s;
`

//火狐浏览器，拖拽变成搜索 禁用
if (!!navigator.userAgent.match(/Firefox/i)) {
	document.body.ondrop = function (event) {
		event.preventDefault()
		event.stopPropagation()
	}
}

const DragDropContext = createContext<DnDContextValue>(null!)

/**
 * 拖拽 Provider，负责一些数据传递，无参数
 */
export const DragDropContextProvider: FC = ({ children }) => {
	const [placeholderIndex, setPlaceholderIndex] = useState<number | null>(null)

	return (
		<DragDropContext.Provider
			value={{
				placeholderIndex,
				setPlaceholderIndex,
			}}
		>
			{children}
		</DragDropContext.Provider>
	)
}

/**
 * 拖拽对象
 * @param style 可选，拖拽对象样式
 * @param index 可选，当前对象在列表中的索引【开启拖拽占位效果时，必须传入】
 * @param dataTransfer: 拖拽对象的原生 dataTransfer 对象，或者返回  dataTransfer 的函数
 * @param canDrag: 可选，是否可拖拽
 * @param ghostRender: 拖拽出来的 ghost ReactNode
 * @param onDragStart: 可选，拖拽开始执行函数
 * @param onDragEnd: 可选，拖拽结束执行函数
 * @param children:  ReactNode(实际渲染的UI)
 */
export function Draggable(props: DraggableProps) {
	const { style, className, index, canDrag = true, dataTransfer, ghostRender } = props
	const dndContext = useContext(DragDropContext)

	let mergeStyle = style
	if (style && index != null && dndContext.placeholderIndex !== null && dndContext.placeholderIndex <= index) {
		mergeStyle = {
			...style,
			transform: `translate(0, 32px)`,
		}
	}

	const handleDragEnd = useCallback(
		(ev: React.DragEvent) => {
			ev.preventDefault()
			ev.stopPropagation()

			props.onDragEnd?.(ev)

			const node = document.getElementById('dnd__container__wrap')
			if (node) {
				document.body.removeChild(node)
			}
		},
		[props.onDragEnd]
	)

	const handleDragStart = useCallback(
		(ev: React.DragEvent) => {
			if (typeof dataTransfer === 'function') {
				ev.dataTransfer.setData('text', JSON.stringify(dataTransfer()))
			} else {
				ev.dataTransfer.setData('text', JSON.stringify(dataTransfer))
			}

			props.onDragStart?.(ev)

			if (ev.dataTransfer.setDragImage) {
				const node = document.createElement('div')
				node.id = `dnd__container__wrap`
				node.style.position = `fixed`
				node.style.top = `0`
				node.style.left = `-9999px`
				node.style.width = `220px`
				document.body.appendChild(node)

				ReactDOM.render(<>{ghostRender}</>, node)

				ev.dataTransfer.setDragImage(node, 1, 10)
			}
		},
		[dataTransfer, props.onDragStart, ghostRender]
	)

	return (
		<Wrap
			className={className}
			style={mergeStyle}
			draggable={canDrag}
			onDragEnd={handleDragEnd}
			onDragStart={handleDragStart}
		>
			{props.children}
		</Wrap>
	)
}

function calculate(total: number, arr: number[] | number) {
	let i = 0
	while (total > 0 && (arr instanceof Array ? arr[i] : arr) > 0) {
		total = total - (arr instanceof Array ? arr[i] : arr)
		i++
	}
	if (total < 0 && (arr instanceof Array ? arr[i - 1] : arr) / 2 < Math.abs(total)) {
		i--
	}
	return i
}

/**
 * 拖拽容器
 * @param onDrop: （可选） (dataTransfer: 拖拽对象的原生 dataTransfer 对象, placeholderIndex: 拖拽最终放置位置) => void
 * @param children: (setDroppableRef: ref对象，获取原生dom节点) => ReactNode（实际渲染的容器UI）
 * @param isPlaceholder（可选） 默认开启拖拽占位效果
 * @param placeholderHeight（可选）默认列表元素占位高度32 , 或者动态高度数组
 */
export function Droppable<T extends boolean = true>(props: DroppableProps<T>) {
	const { onDrop, placeholderHeight, isPlaceholder, children } = props

	const enterCount = useRef<number>(0)
	const ie10 = useRef<number>(0)

	const listRef = useRef<HTMLElement | null>(null)
	const setDroppableRef = useCallback((value) => {
		listRef.current = value
	}, [])
	const dndContext = useContext(DragDropContext)

	const handleDragOver = useCallback(
		(ev: DragEvent) => {
			ev.preventDefault()
			ev.stopPropagation()

			if (!listRef.current) return
			const rect = listRef.current.getBoundingClientRect()
			let top = rect.top
			let scrollTop = listRef.current.scrollTop
			let clientY = ev.clientY

			let index = calculate(clientY - top + scrollTop, placeholderHeight)

			if (window.navigator.userAgent.indexOf('MSIE') > 0) {
				let target_index = index - calculate(ev.offsetY, placeholderHeight)
				if (ie10.current < 15 && dndContext.placeholderIndex !== null) {
					index = dndContext.placeholderIndex
					ie10.current = ie10.current + 1
				} else {
					index = target_index
					ie10.current = 0
				}
			}

			if (isPlaceholder) {
				dndContext.setPlaceholderIndex(index)
			}
		},
		[dndContext, isPlaceholder, placeholderHeight]
	)

	const handleDrop = useCallback(
		(ev: DragEvent) => {
			ev.preventDefault()
			ev.stopPropagation()

			let text = ev.dataTransfer && ev.dataTransfer.getData('text')

			if (text) {
				try {
					let dataTransfer = JSON.parse(text)
					onDrop(dataTransfer, dndContext.placeholderIndex as T extends true ? number : null, ev)
				} catch (err) {}
			}

			enterCount.current = 0

			if (isPlaceholder) {
				dndContext.setPlaceholderIndex(null)
			}
		},
		[dndContext, isPlaceholder, onDrop]
	)

	const handleDragEnter = useCallback((ev: DragEvent) => {
		ev.preventDefault()
		ev.stopPropagation()

		enterCount.current = enterCount.current + 1
	}, [])

	const handleDragleave = useCallback(
		(ev: DragEvent) => {
			ev.preventDefault()
			ev.stopPropagation()

			enterCount.current = enterCount.current - 1

			if (isPlaceholder && enterCount.current === 0) {
				dndContext.setPlaceholderIndex(null)
			}
		},
		[dndContext, isPlaceholder]
	)

	useEffect(() => {
		if (listRef.current) {
			listRef.current.addEventListener('dragover', handleDragOver)
			listRef.current.addEventListener('dragenter', handleDragEnter)
			listRef.current.addEventListener('drop', handleDrop)
			listRef.current.addEventListener('dragleave', handleDragleave)
		}
		return () => {
			if (listRef.current) {
				listRef.current.removeEventListener('dragover', handleDragOver)
				listRef.current.removeEventListener('dragenter', handleDragEnter)
				listRef.current.removeEventListener('drop', handleDrop)
				listRef.current.removeEventListener('dragleave', handleDragleave)
			}
		}
	}, [handleDragEnter, handleDragOver, handleDragleave, handleDrop])

	return <>{children(setDroppableRef)}</>
}

Droppable.defaultProps = {
	isPlaceholder: true,
	placeholderHeight: 32,
	onDrop: () => {},
}
